/**
* file name  : it/ciano/jenoware/Pedigree.java
* authors    : Luciano Xumerle
* created    : lun 06 set 2004 10:59:14 CEST
*
* Copyright (c) 2004-2010 Luciano Xumerle. All rights reserved.
*
* This file is part of gephinfo.
*
* gephinfo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*/

package it.ciano.jenoware;

import java.io.*;
import java.util.*;
import java.util.regex.*;

import it.ciano.util.*;

/**
 * The class manage every information related with a set of families (Family objects).
 * The HashMap key is a family identifier and value is a Family class object.
 *
 * @author Luciano Xumerle
 * @version 0.0.1
 */
public class Pedigree extends HashMap {

    static final long serialVersionUID = 4870927433255813036L;

    /**
     * Map each ID to one or more family.
     * Key is ID and value is Sbj object.
     */
    private HashMap _id2sbj;

    /**
     * The marker list.
     * Values are String.
     */
    private FieldList _markerSet;

    /**
     * The phenotype list.
     * Values are String.
     */
    private FieldList _phenoSet;

    /**
     * Class constructor.
     */
    public Pedigree () {
        super();
        _markerSet = new FieldList();
        _phenoSet = new FieldList();
        _id2sbj = new HashMap();
    }

    /**
     * Returns a new Pedigree object in which the families' identifiers are
     * stores as integer values.
     * All the other data is the same. The Sbj are not cloned while creating the new Pedigree
     * and the new family ID.
     *
     * @return a new Pedigree object.
     */
    public Pedigree setFamilyID2Integer() {
        // create the new Pedigree
        Pedigree pedi=new Pedigree();

        // add the markers list
        String[] mm=getMarkersList();
        if ( mm!=null )
            for ( int i=0; i<mm.length; i++ )
                pedi.addMarker2List ( mm[i] );

        // add the phenotypes list
        String[] pp=getPhenotypesList();
        if ( pp!=null )
            for ( int i=0; i<pp.length; i++ )
                pedi.addPhenotype2List ( pp[i] );

        // add all the sbj with the new family id set to number
        Family[] list=this.getFamilyList();
        for ( int i=0; i<list.length; i++ ) {
            String fam_num = "" + ( i+1 );
            Sbj[] ss= list[i].getSbjs();
            for ( int j=0; j<ss.length; j++ ) {
                ss[j].setFML ( fam_num );
                pedi.addSbj ( ss[j] );
            }
        }
        return pedi;
    }

    /**
     * The method adds the Sbj object to the Pedigree.
     * If the subject is unrelated and the fields fml and sbj have no valid
     * values, then the method will create a Family having only one subject
     * with family pedigree identifier (fml) = subject identifier (id) =
     * subject pedigree identifier (sbj).
     *
     * @param entry the Sbj object.
     * @return true or false;
     */
    public boolean addSbj ( Sbj entry ) {
        if ( entry == null )
            return false;
        // id
        String id = entry.getID();

        // update phenotype list
        String pheno[] = entry.getPhenotypesList();
        if ( pheno != null )
            for ( int i = 0; i < pheno.length; i++ )
                addPhenotype2List ( pheno[ i ] );

        // update genotype list
        String markers[] = entry.getMarkersList();
        if ( markers != null )
            for ( int i = 0; i < markers.length; i++ )
                addMarker2List ( markers[ i ] );

        // merge info if present
        if ( _id2sbj.containsKey ( id ) )
            entry.merge ( getSBJ ( id ) );


        // family list
        String[] fams = entry.getFML();

        // is related and the family already inserted?
        if ( fams!=null ) {
            for ( int ff=0; ff<fams.length; ff++ ) {
                if ( !this.containsKey ( fams[ff] ) )
                    this.put ( fams[ff], new Family ( fams[ff] ) );
                ( ( Family ) this.get ( fams[ff] ) ).addSbj ( entry );
            }
        }

        // insert new updated Sbj
        _id2sbj.put ( id, entry );

        return true;
    }


    /**
     * Adds the faked Sbj object to the Pedigree.
     * A faked Sbj is added automatically to complete a family (ie. missing parent).
     *
     * @param entry the Sbj object.
     * @return true or false.
     */
    public boolean addSbjAsFaked ( Sbj entry ) {
        if ( entry == null )
            return false;
        entry.setAsFaked();
        return addSbj ( entry );
    }

    /**
     * Returns the Sbj object for a given fml and sbj.
     * Method checks if fml and sbj are not equal to 0.
     * Returns null if sbj doesn't exist.
     *
     * @param fml the family identifier.
     * @param sbj the unique subject number in the family.
     * @return the Sbj object (null if there is no value).
     */
    public Sbj getSBJ ( String fml, String sbj ) {
        Family ff = ( Family ) this.get ( fml );
        if ( ff != null )
            return ff.getSBJ ( sbj );
        return null;
    }


    /**
     * Returns the Sbj object.
     *
     * @param id the ID of a subject (Sbj.getID()).
     * @return true if ID exist in almost one family and id!=null.
     */
    public Sbj getSBJ ( String id ) {
        if  ( id != null && _id2sbj.containsKey ( id ) )
            return ( Sbj ) _id2sbj.get ( id );
        return null;
    }


    /**
     * Returns true if ID exist in almost one family and id!=null.
     *
     * @param id the ID of a subject (Sbj.getID()).
     * @return true if ID exist in almost one family and id!=null.
     */
    public boolean existID ( String id ) {
        if ( id == null )
            return false;
        return ( _id2sbj.containsKey ( id ) );
    }

    /**
     * Returns the ID.
     *
     * @param fml the family number.
     * @param sbj the unique subject number in the family.
     * @return the ID (null if there is no value).
     */
    public String getID ( String fml, String sbj ) {
        Family ff= ( Family ) this.get ( fml );
        if ( ff!=null )
            return ff.getSbjID ( sbj );
        return null;
    }

    /**
     * Returns a list of all Family objects stored in the Pedigree.
     *
     * @return the list of Family objects
     */
    public Family[] getFamilyList() {
        Family res[] = new Family[ size() ];
        int i = 0;
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            res[ i++ ] = ( Family ) entry.getValue();
        }
        return res;
    }

    /**
     * Returns an array containing all the families in which the ID is stored.
     *
     * @param id the ID.
     * @return the String array (null if there is no value).
     */
    public String[] getFamiliesFromID ( String id ) {
        Sbj tt = getSBJ ( id );
        if ( tt == null )
            return null;
        return tt.getFML();
    }

    /**
     * Returns an array with the two elements {"father", "mother"}.
     * If father or mother not exist, then create the needed faked Sbj.
     *
     * @param sbj the Sbj object.
     * @return the Sbj array (null if the input Sbj is null or faked).
     */
    public Sbj[] getParents ( Sbj sbj ) {
        return ( ( Family ) this.get ( sbj.getFML() ) ).getParents ( sbj );
    }

    /**
     * Returns an array with the two Genotype of father and mother.
     *
     * @param sbj the Sbj.
     * @param marker the marker name.
     * @return array with {"father's Genotype", "mother's Genotype"}.
     */
    public Genotype[] getParentsMarker ( Sbj sbj, String marker ) {
        return ( ( Family ) this.get ( sbj.getFML() ) ).getParentsMarker ( sbj, marker );
    }

    /**
     * Returns a Sbj array containing the Sbj's brothers.
     *
     * @param sbj the Sbj object.
     * @return the Sbj array (null if there is no brother).
     */
    public Sbj[] getBros ( Sbj sbj ) {
        if ( containsKey ( sbj.getFML() ) )
            return ( ( Family ) this.get ( sbj.getFML() ) ).getBros ( sbj );
        return null;
    }

    /**
     * Returns true if the Sbj has almost one brother.
     *
     * @param sbj the Sbj object.
     * @return true or false.
     */
    public boolean hasBros ( Sbj sbj ) {
        if ( containsKey ( sbj.getFML() ) )
            return ( ( Family ) this.get ( sbj.getFML() ) ).hasBros ( sbj );
        return false;
    }

    /**
     * Returns a Sbj array containing the Sbj's sons.
     *
     * @param sbj the Sbj object.
     * @return the Sbj array (null if there is no son).
     */
    public Sbj[] getSons ( String fml, Sbj sbj ) {
        if ( containsKey ( fml ) )
            return ( ( Family ) this.get ( fml ) ).getSons ( sbj );
        return null;
    }

    /**
     * Returns true if the Sbj has almost one son.
     *
     * @param sbj the Sbj object.
     * @return true or false.
     */
    public boolean hasSons ( Sbj sbj ) {
        if ( containsKey ( sbj.getFML() ) )
            return ( ( Family ) this.get ( sbj.getFML() ) ).hasSons ( sbj );
        return false;
    }

    /**
     * Returns an Allele array containing every valid allele in the family.
     *
     * @param sbj the Sbj object.
     * @param marker the marker name.
     * @return Allele array (null if there is no value).
     */
    public Allele[] getFamilyAllelesFromSbj ( Sbj sbj, String marker ) {
        if ( sbj != null && containsKey ( sbj.getFML() ) )
            return ( ( Family ) this.get ( sbj.getFML() ) ).getObservedAlleles ( marker );
        return null;
    }

    /**
     * Returns a Sbj array containing every Sbj of the Family.
     *
     * @param fam the identifier of a family.
     * @return the Sbj array (null if there is no value).
     */
    public Sbj[] getFamSBJs ( String fam ) {
        if ( fam != null && containsKey ( fam ) )
            return ( ( Family ) this.get ( fam ) ).getSbjs();
        return null;
    }

    /**
     * Returns a String array containing every ID of the Family.
     *
     * @param sbj the Sbj object.
     * @return the array (null if there is no value).
     */
    public String[] getFamilyIDs ( Sbj sbj ) {
        if ( containsKey ( sbj.getFML() ) )
            return ( ( Family ) this.get ( sbj.getFML() ) ).getSbjIDs();
        return null;
    }

    /**
     * Returns a the Family object containing the given subject.
     *
     * @param sbj the Sbj object.
     * @return the family.
     */
    public Family getFamily ( Sbj sbj ) {
        if ( containsKey ( sbj.getFML() ) )
            return  ( Family ) this.get ( sbj.getFML() );
        return null;
    }

    /**
     * Returns the sorted Sbj array containing every Sbj in the Pedigree object.
     *
     * @return the sorted Sbj array.
     */
    public Sbj[] getListSbj() {
        ArrayList temp = new ArrayList();
        // for each family extract the list
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            temp.addAll ( ( ( Family ) entry.getValue() ).getListSbj() );
        }
        for ( Iterator it = _id2sbj.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Sbj t= ( Sbj ) entry.getValue();
            if ( ! t.hasFamily() )
                temp.add ( t );
        }

        Sbj[] tt = new Sbj[ temp.size() ];
        for ( int i = 0; i < temp.size(); i++ )
            tt[ i ] = ( Sbj ) temp.get ( i );
        Arrays.sort ( tt );
        return tt;
    }

    /**
     * Returns the sorted Sbj array containing every faked Sbj in the Pedigree object.
     *
     * @return the Sbj array.
     */
    public Sbj[] getFakedListSbj() {
        ArrayList temp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Family tt = ( Family ) entry.getValue();
            temp.addAll ( tt.getListFakedSbj() );
        }
        Collections.sort ( temp );
        Sbj[] tt = new Sbj[ temp.size() ];
        for ( int i = 0; i < temp.size(); i++ )
            tt[ i ] = ( Sbj ) temp.get ( i );
        return tt;
    }

    /**
     * Returns a sorted Sbj array containing every genotyped Sbj in the Pedigree object.
     *
     * @return the Sbj array.
     */
    public Sbj[] getGenotypedSbj() {
        ArrayList temp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Family tt = ( Family ) entry.getValue();
            temp.addAll ( tt.getGenotypedSbj() );
        }
        Collections.sort ( temp );
        Sbj[] tt = new Sbj[ temp.size() ];
        for ( int i = 0; i < temp.size(); i++ )
            tt[ i ] = ( Sbj ) temp.get ( i );
        return tt;
    }

    /**
     * Returns a sorted Sbj array containing every unrelated Sbj in the Pedigree object.
     *
     * @return the Sbj array.
     */
    public Sbj[] getUnrelatedSbj() {
        ArrayList temp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Family tt = ( Family ) entry.getValue();
            temp.addAll ( tt.getUnrelatedSbj() );
        }
        Sbj[] tt = new Sbj[ temp.size() ];
        for ( int i = 0; i < temp.size(); i++ )
            tt[ i ] = ( Sbj ) temp.get ( i );
        Arrays.sort ( tt );
        return tt;
    }

    /**
     * Returns a sorted Sbj array containing every proband Sbj in the Pedigree object.
     *
     * @return the Sbj array.
     */
    public Sbj[] getProbandSBJs() {
        ArrayList temp = new ArrayList();
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = ( Map.Entry ) it.next();
            Family tt = ( Family ) entry.getValue();
            Sbj ss = tt.getProband();
            if ( ss != null )
                temp.add ( ss );
        }
        Sbj[] tt = new Sbj[ temp.size() ];
        for ( int i = 0; i < temp.size(); i++ )
            tt[ i ] = ( Sbj ) temp.get ( i );
        Arrays.sort ( tt );
        return tt;
    }

    /**
     * Returns the list of phenotypes stored in the Pedigree object.
     *
     * @return the list (null if there is no phenotype).
     */
    public String[] getPhenotypesList() {
        return _phenoSet.getFields();
    }

    /**
     * Adds the phenotype to the list.
     * The list of phenotypes has the same order of the input order.
     *
     * @param pheno the phenotype name.
     */
    public void addPhenotype2List ( String pheno ) {
        _phenoSet.add ( pheno );
    }

    /**
     * Returns the list of markers stored in the Pedigree object.
     *
     * @return the list (null if there is no marker).
     */
    public String[] getMarkersList() {
        return _markerSet.getFields();
    }

    /**
     * Adds the marker to the list.
     * The list of markers has the same order of the input order.
     *
     * @param marker the marker name.
     */
    public void addMarker2List ( String marker ) {
        _markerSet.add ( marker );
    }

    /**
     * The method scans every family and does the mendelian check for the given marker.
     *
     * @param marker the marker name.
     * @return the results (empty String if there is no error).
     */
    public String pedCheck ( String marker ) {
        StringBuffer out = new StringBuffer();
        out.append ( "++ PedCheck for marker: " );
        out.append ( marker );
        out.append ( "\n" );
        int counter = 0;
        int total = 0;
        for ( Iterator it = entrySet().iterator(); it.hasNext(); ) {

            Map.Entry entry = ( Map.Entry ) it.next();
            Family cur = ( Family ) entry.getValue();
            String ped = cur.pedCheck ( marker );
            if ( !ped.equals ( "" ) ) {
                out.append ( ped );
                counter++;
            }
            total++;
        }
        out.append ( "\n" );
        out.append ( "\n++ Parsed families for  this marker: " );
        out.append ( total );
        out.append ( "\n++ Parsed families with some errors: " );
        out.append ( counter );
        out.append ( "\n\n" );
        return out.toString();
    }


    /**
     * Returns an array of Pedigree objects. The number of Pedigree is the same
     * of the number of <i>setSymbols</i> given in input.
     * The Pedigree at index 0 contains the genotyped subjects having
     * ( <i>phenotype</i> == <i>setSymbols[0]</i> ) and,
     * the Pedigree at index <i>i</i> contains the genotyped subjects having
     * ( <i>phenotype</i> == <i>setSymbols[i]</i> ).
     *
     * @param phenotype the phenotype used to stratify the subjects.
     * @param setSymbols the list of phenotype values used to split subjects.
     * @return the array containing the two Pedigree objects.
     */
    public Pedigree[] stratify ( String phenotype, String[] setSymbols ) {

        Pedigree[] res=new Pedigree[setSymbols.length];
        for ( int i=0; i<setSymbols.length; i++ )
            res[i]=new Pedigree();

        // get the genotyped ids
        Sbj[] ids=getGenotypedSbj();

        for ( int i=0; i<ids.length; i++ ) {
            String p;
            if ( phenotype.equals ( "sex" ) )
                p=ids[i].getSEX();
            else if ( phenotype.equals ( "prb" ) ) {
                String[] fams=ids[i].getFML();
                p="0";
                for ( int ff=0; ff<fams.length; ff++ )
                    if ( ids[i].getProband ( fams[ff] ).equals ( "1" ) )
                        p="1";
            } else
                p=ids[i].getPhenotype ( phenotype ).getValue();

            for ( int s=0; s<setSymbols.length; s++ )
                if ( p.equals ( setSymbols[s] ) ) {
                    res[s].addSbj ( ids[i] );
                    break;
                }
        }
        return res;
    }

} // end class

